#!/usr/bin/env python3

# Copyright (c) 2020-2021 Fpemud <fpemud@sina.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.


import os
import abc
import stat
import json
import pathlib
import functools
from ._util import Util, ActionRunner
from ._errors import WorkDirError
from ._storage_layout import StorageLayout


class WorkDir:
    """
    This class manipulates wstage4's working directory.
    """

    def __init__(self, path, reset=False):
        assert path is not None

        self._MODE = 0o40700

        self._path = path
        self._addon = None

        self._tsFile = os.path.join(path, "target-settings.save")
        if os.path.exists(self._tsFile):
            with open(self._tsFile) as f:
                self._ts = json.load(f)
        else:
            self._ts = None

        self._imageLayoutFile = os.path.join(path, "disk.layout")
        self._imageFile = os.path.join(path, "disk.img")
        self._efiVarFile = os.path.join(path, "efi-var.fd")

        self._vmCfgFile = os.path.join(path, "vm-cfg.save")
        self._vmCmdFile = os.path.join(path, "vm-cmd.save")

        self._vmDisplaySocketFile = os.path.join(path, "vm-display.socket")
        self._vmCtrlSocketFile = os.path.join(path, "vm-ctrl.socket")
        self._vmRecordFilePrefix = os.path.join(path, "boot-record-")
        self._vmScreenDumpFile = os.path.join(path, "vm-screen.ppm")

        self._persistentStorage = WorkDirPersisentStorage(self)

        if not os.path.exists(self._path):
            os.mkdir(self._path, mode=self._MODE)
        else:
            # work directory can be a directory or directory symlink
            # so here we use os.stat() instead of os.lstat()
            s = os.stat(self._path)
            if not stat.S_ISDIR(s.st_mode):
                raise WorkDirError("\"%s\" is not a directory" % (self._path))
            if s.st_mode != self._MODE:
                raise WorkDirError("invalid mode for \"%s\"" % (self._path))
            if s.st_uid != os.getuid():
                raise WorkDirError("invalid uid for \"%s\"" % (self._path))
            if s.st_gid != os.getgid():
                raise WorkDirError("invalid gid for \"%s\"" % (self._path))

            # clear directory content if needed
            if reset:
                Util.removeDirContentExclude(self._path, [])

    @property
    def path(self):
        return self._path

    @property
    def image_filepath(self):
        return self._imageFile

    @property
    def vm_display_socket_filepath(self):
        return self._vmDisplaySocketFile

    @property
    def vm_control_socket_filepath(self):
        return self._vmCtrlSocketFile

    def attach_addon(self, workdir_addon):
        assert self._addon is None
        assert isinstance(workdir_addon, WorkDirAddonBase)
        self._addon = workdir_addon

    def has_error(self):
        assert not self._persistentStorage.isInUse()
        _, err = self._persistentStorage.initGetCurrentActionInfo()
        return err is not None

    def get_error_message(self):
        assert not self._persistentStorage.isInUse()
        actionName, err = self._persistentStorage.initGetCurrentActionInfo()
        return "action %s failed (%s) for \"%s\"" % (actionName, err, self._path)

    @functools.cache
    def get_image_file_storage_layout_name(self):
        return pathlib.Path(self._imageLayoutFile).read_text().rstrip("\n")

    def get_image_file_size(self):
        return os.path.getsize(self._imageFile)

    def enlarge_image_file_to(self, final_size):
        return StorageLayout.get(self.get_image_file_storage_layout_name()).enlarge_to(self._imageFile, self._path, final_size)

    def mount_image_file(self, stage=-1):
        return StorageLayout.get(self.get_image_file_storage_layout_name()).mount(self._imageFile, self._path, stage)

    def is_image_file_mounted(self):
        return StorageLayout.get(self.get_image_file_storage_layout_name()).is_mounted(self._path, 1)

    def umount_image_file(self):
        StorageLayout.get(self.get_image_file_storage_layout_name()).umount(self._path)

    def is_build_finished(self):
        return self._persistentStorage.isFinished()

    def _saveTargetSettings(self, ts):
        data = ts.to_dict()
        with open(self._tsFile, "w") as f:
            json.dump(data, f, indent=4)
        self._ts = data

    def _createImageFile(self, fileSize, storageLayoutName):
        assert not os.path.exists(self._imageLayoutFile)
        assert not os.path.exists(self._imageFile)
        try:
            with open(self._imageLayoutFile, "w") as f:
                f.write(storageLayoutName + "\n")
            with open(self._imageFile, 'wb') as f:
                f.truncate(fileSize)
            StorageLayout.get(storageLayoutName).initialize(self._imageFile, self._path)
        except BaseException:
            Util.forceDelete(self._imageFile)
            Util.forceDelete(self._imageLayoutFile)
            raise

    def _saveVm(self, vmConfig, vmCmd):
        with open(self._vmCfgFile, "w") as f:
            f.write(vmConfig.export_to_file_as_ini_content())
        with open(self._vmCmdFile, "w") as f:
            f.write(vmCmd + "\n")

    def __getattr__(self, attr):
        if self._addon is not None:
            return getattr(self._addon, attr)
        raise AttributeError(f"'{self.__class__.name}' object has no attribute '{attr}'")       # ugly, from PEP-562, a string which is same as system error message


class WorkDirAddonBase(abc.ABC):

    def __init__(self):
        self._path = None


class WorkDirPersisentStorage(ActionRunner.PersistStorage):

    def __init__(self, parent):
        self._parent = parent
        self._currentFile = os.path.join(self._parent.path, "builder-run.save")                  # contains current action name and error
        self._historyFile = os.path.join(self._parent.path, "builder-history-actions.save")      # contains names of all completed actions (current action is not included)
        self._finishFile = os.path.join(self._parent.path, "builder-finished.flag")
        self._inUse = False

    def initGetCurrentActionInfo(self):
        try:
            tlist = pathlib.Path(self._currentFile).read_text().rstrip("\n").split("\n")
            if len(tlist) > 1:
                # action failed
                actionName = tlist[0]
                error = tlist[1]
            else:
                # action is running
                actionName = tlist[0]
                error = None
        except FileNotFoundError:
            # no action running, last action finished successfully
            actionName = None
            error = None

        if actionName is not None:
            if error is None:
                error = "crashed"
        else:
            if os.path.exists(self._parent.image_filepath):
                if StorageLayout.get(self._parent.get_image_file_storage_layout_name()).is_dirty(self._parent.image_filepath, self._parent.path):
                    actionName = self.getHistoryActionNames()[-1]
                    error = "crashed"

        return (actionName, error)

    def getHistoryActionNames(self):
        try:
            return pathlib.Path(self._historyFile).read_text().rstrip("\n").split("\n")
        except FileNotFoundError:
            return []

    def isFinished(self):
        return os.path.exists(self._finishFile)

    def isInUse(self):
        return self._inUse

    def use(self):
        assert not self._inUse
        self._inUse = True

    def saveActionStart(self, actionName):
        assert self._inUse
        assert not os.path.exists(self._currentFile)
        with open(self._currentFile, "w") as f:
            f.write(actionName + "\n")

    def saveActionEnd(self, error=None):
        assert self._inUse
        assert os.path.exists(self._currentFile)
        if error is None:
            with open(self._historyFile, "a") as f:
                f.write(pathlib.Path(self._currentFile).read_text())        # append action to history
            os.unlink(self._currentFile)
        else:
            with open(self._currentFile, "a") as f:
                f.write(error + "\n")

    def saveFinished(self):
        assert self._inUse
        assert not os.path.exists(self._finishFile)
        with open(self._finishFile, "w") as f:
            f.write("")

    def unUse(self):
        assert self._inUse
        self._inUse = False
